Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QM duration sweeper using multiple waveforms #979

Merged
merged 18 commits into from
Aug 24, 2024
Merged

Conversation

stavros11
Copy link
Member

@stavros11 stavros11 commented Aug 13, 2024

Follows the plan outlined in #935 (comment), which I copy here in order to track progress:

  • implement duration sweepers in QM with multiple waveforms
  • checking the timing is predictable, and it could sweep a Rabi with just delays wait() instructions
  • introduce a new DurationInterpolated sweeper parameter
  • introduce an Align pulse, to synchronize multiple channels
    • this will have the specific meaning of a runtime synchronization, possibly introducing delays
  • keep Sequence as a list, in such a way that is clear for all channels what it comes before and after an Align (done in Sequence internal layout #965)

Example report for the Rabi length routine: http://login.qrccluster.com:9000/3XbCXPG-TAGC5uODkQDGFQ==

There is still an issue at short times (< 30ns) however I believe this is not due to the scheduling, as it appears even when using qua.align() and qua.switch_() with multiple waveforms. With the 1 clock cycle (=4ns) padding on delays, there is no overlap between the drive and readout pulses, as shown in the following simulation.

Simulation plot

Screenshot 2024-08-13 205027

Copy link

codecov bot commented Aug 13, 2024

Codecov Report

Attention: Patch coverage is 53.53535% with 46 lines in your changes missing coverage. Please review.

Project coverage is 42.88%. Comparing base (e8abaa5) to head (4e4c2a5).
Report is 19 commits behind head on 0.2.

Files Patch % Lines
src/qibolab/instruments/qm/controller.py 22.58% 24 Missing ⚠️
src/qibolab/instruments/qm/program/instructions.py 16.66% 20 Missing ⚠️
src/qibolab/instruments/qm/program/sweepers.py 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              0.2     #979      +/-   ##
==========================================
+ Coverage   42.76%   42.88%   +0.12%     
==========================================
  Files          80       80              
  Lines        5563     5624      +61     
==========================================
+ Hits         2379     2412      +33     
- Misses       3184     3212      +28     
Flag Coverage Δ
unittests 42.88% <53.53%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@alecandido alecandido added this to the Qibolab 0.2.0 milestone Aug 14, 2024
@alecandido alecandido linked an issue Aug 15, 2024 that may be closed by this pull request
@stavros11
Copy link
Member Author

@alecandido in 8ec214b (and 6c45843) I added a new Align pulse-like object and provided an interface to use it through the PulseSequence as well as a function to compile it to delays. I still need to test on hardware and implement the DurationInterpolated sweep parameter. In the meantime you can check if you agree with this Align if you like.

I keep this based on 0.2 because I am not sure if you are planning to push something new in the other "tower of PRs" (starting from #970) and it is also more or less independent (there will be conflicts from ChannelId but will be easy to fix). As soon as we merge the other PRs, which will probably happen sooner than this, I will rebase it.

@alecandido
Copy link
Member

In the meantime you can check if you agree with this Align if you like.

I will

I keep this based on 0.2 because I am not sure if you are planning to push something new in the other "tower of PRs" (starting from #970) and it is also more or less independent (there will be conflicts from ChannelId but will be easy to fix). As soon as we merge the other PRs, which will probably happen sooner than this, I will rebase it.

No worries, PRs should be mostly independent. Now we were working ignoring a bit that principle (I was the one stacking PRs in the first place), but they could always be rebased onto each other. Especially when a reduce portion of the codebase was involved (so, reduced risk of conflicts). We will rebase even this, no worries.

Copy link
Member

@alecandido alecandido left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only checked the non-QM part, and it seems good to me :)

src/qibolab/sequence.py Outdated Show resolved Hide resolved
src/qibolab/sequence.py Outdated Show resolved Hide resolved
self.append((channel, align))
return align

def align_to_delays(self) -> "PulseSequence":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle, this could be a property as well.

Since it is sometimes quite subjective what could a property, I personally adopted the criterion that a method without parameters (other than self) is always a suitable property. The complicate part might be finding a suitable name :P

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was expecting this comment, however I was not sure because trim() (the next function of PulseSequence) is a very similar case and you did not make it a property. I'd say function makes a bit more sense to me for both these cases, because there is some operation (compilation) involved, but I would also be fine with properties.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're definitely right. I could make it .trimmed.

But yes, it seems neat also as a function, because we know it's doing something (while from a property you generally expect to do little). Problem is that "something" is ill-defined (in a sense, each property does something, otherwise it would be an attribute), that's why the criterion above...

src/qibolab/sequence.py Show resolved Hide resolved
tests/pulses/test_sequence.py Outdated Show resolved Hide resolved
@stavros11
Copy link
Member Author

This is now ready, including the interpolated duration sweeper. I tested the single shot, Rabi amplitude and Rabi length (with and without interpolation) on hardware and they all seem to work.

@stavros11 stavros11 marked this pull request as ready for review August 22, 2024 18:30
@stavros11 stavros11 requested a review from alecandido August 22, 2024 18:32
src/qibolab/sequence.py Outdated Show resolved Hide resolved
Comment on lines +65 to +68
# keep track of ``Align`` command that were already played
# because the same ``Align`` will appear on multiple channels
# in the sequence
processed_aligns = set()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether at this level it is just simpler to preprocess the sequence, and replace Align by a plain list of channels, while replacing the channel e.g. with an empty string, or the "align" string. And then, if channel_id == "align" you do qua.align(*pulse).
It's not that elegant from the point of view of the types (though it could be made more elegant - but it's not needed), still it should be much simpler (and you avoid iterating the whole sequence every time that you encounter an align, also because you know they are supposed to be consecutive, otherwise it would be a bug).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that elegant from the point of view of the types (though it could be made more elegant - but it's not needed), still it should be much simpler (and you avoid iterating the whole sequence every time that you encounter an align, also because you know they are supposed to be consecutive, otherwise it would be a bug).

Indeed, my main issue with this is that it is not that elegant and may require more code from the driver side, like having a QM internal sequence, while I am trying to rely on qibolab primitives as much as possible. Also, the solution with the set shouldn't be much overhead in terms of performance and it is also more localized, just a few additional lines of code in a single place. The preprocess would probably have to happen somewhere else and copy the whole sequence, just to handle Align which is a very special and potentially rare case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set is not the problem, but rather

channel_ids = args.sequence.pulse_channels(pulse.id)

which requires iterating the whole sequence for every Align (for each ID, not each combination (ch, Align), of course).

I was not proposing a separate sequence, just a single (inelegant) function doing:

def process_aligns(sequence: PulseSequence) -> list[Union[tuple[ChannelId, PulseLike], tuple[Literal["align"], set[ChannelId]]]]:
    new = []
    align_channels = set()
    def align():
        if len(align_channels) > 0:
            new.append(("align", align_channels))
            align_channels.clear()
    for (ch, p) in sequence:
        if isinstance(p, Align):
            align_channels.add(ch)
        else:
            align()
            new.append((ch, p))
    align()
    return new

You can tell it's not elegant from the return type (though it could be written better than that), but it requires a single iteration, without any QM-specific object (it could even be part of the sequence API...).

However, as I said, it's negligible. I'm just obsessed...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for the clarification. That is fine with me and I could add it to the code. Maybe a simplification would be to use the actual Align object in the tuple, instead of Literal["align"] and have the following return type:

list[tuple[Union[ChannelId, set[ChannelId]], PulseLike]]

It isn't much simpler, it's just that the second tuple element is always PulseLike. We could even get rid of the Union if we convert the first element to be a list or set always, with single element for non-aligns.

My issue with all these solutions is that, although they are more efficient when Aligns are present, they are slightly less efficient when Aligns are not present. For example in this case we'd have to copy the sequence even when Align is not present. We could check and only call process_aligns if they are actually present, but even this check would be worst case O(n) (n=len(sequence)) since we'd have to check the whole sequence. In summary, the proposed solution would be O(2n) in all cases, while the current implementation is O((m + 1)n) (m=number of aligns), so if this is correct it depends on what we expect more often. For example the current compiler doesn't use Align (and most qibocal routines will not either) so if we optimize for circuits the current implementation may actually be better in most cases. Of course negligibly better, but since we are discussing this anyway...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in practice the trade-off is even less clear, because the operations involved are not all equivalent: in one case you're just copying some Align (though iterating more PulseLike, if many Align are present), but the other is possibly doing fewer operations, but always copying the whole sequence. So, if copying were much more expensive than iterating, we had a winner...

In any case, as I said, it's just for the sake of discussion. The truth is that this part is not the performance bottleneck.
(already loading the whole platform, creating many objects, and parsing the whole parameters.json, irrespectively of the sequence, should be much more relevant - and still negligible, since most of the execution time in current experiments will be always the actual runtime on the device, and then the networking required)

"""Sweeping pulse duration using distinctly uploaded waveforms."""
with qua.switch_(parameters.duration, unsafe=True):
for value, sweep_op in parameters.pulses:
with qua.case_(value // 4):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is to avoid baking. What happens for the values covered multiple times?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And isn't there a // 4 already in the normalizing function?

Copy link
Member Author

@stavros11 stavros11 Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now this is not related to baking, but it will most likely need to be modified when baking is implemented.

Basically, the reason we need the // 4 in case_ is because it also exists in the normalizing function. In particular, the loop variable (parameters.duration) will take values from an array that was normalized with // 4, while the uploaded waveforms are enumerated with the original array, so we have to divide the enumerator when we do the matching, in order to properly match with the loop variable. We could drop the normalization in both places, however then we would need to make sure to normalize when delays are involved in the same sweeper (eg. Rabi length sweeps the duration of drive pulse and readout delay with the same sweeper), because qua.wait expects clock cycles (=4ns).

Anyway, this will probably change when baking (=supporting pulses with 1ns resolution, the best we can currently do) is implemented, so I would postpone until then, as I am planning to do it right after this PR anyway, together with the other issues from #969. Otherwise, I could push baking directly here, as I don't expect it to be very complicated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so I get that if you're receiving as values [4, 5, 6, 7] you'll sweep four elements, but the value will always be the same one.

For as long as it is documented, for me it's perfectly acceptable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, these values are not supported yet since baking is not implemented, so only waveforms with length that is multiple of 4 are supported (and >16). I am not sure what will be the exact behavior as I haven't tried, but I would guess that an error will appear if we try to upload a waveform of these lengths. In any case, I will implement this before we release 0.2 so it should be fine. For now I added a reference to this discussion in #969.

One additional point to check is what happens to the interpolated sweep in this case. I guess for these lengths it will fail because they are very short, however for something like [20, 21, 22, 23] it may not fail and actually return four overlapping points. If that happens we may want to raise an error ourselves and prompt to use non-interpolated sweep (which will support baking).

@alecandido
Copy link
Member

The only serious doubt is about the switch_ ... case_. Other than that, everything is fine, and comments are just suggestions for code improvements, no additional feature.

Other than that, I would have approved immediately.

@stavros11
Copy link
Member Author

Since this is approved and #1001 is already based on it I will merge.

The open discussions are already referenced in issues. Baking, which is the most relevant, will be addressed immediately in a new PR, while the Align processing I would keep open for now.

@stavros11 stavros11 merged commit e7a8d18 into 0.2 Aug 24, 2024
28 checks passed
@stavros11 stavros11 deleted the qm-duration-sweeps branch August 24, 2024 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pulse scheduling and duration sweepers
2 participants